package com.robusta.app.model.spreadsheet.persistence;

import com.google.common.base.Predicate;
import com.robusta.app.domain.spreadsheet.Model;
import com.robusta.app.model.persistence.AuditListener;
import com.robusta.app.model.persistence.Creatable;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Collection;
import java.util.NoSuchElementException;
import java.util.Set;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.collect.Sets.filter;
import static com.google.common.collect.Sets.newHashSet;

@Entity @Table(name = "MODEL") @EntityListeners(AuditListener.class)
public class PersistableModel extends com.robusta.app.model.persistence.Entity implements Model, Serializable, Creatable {
    @Id
    @Column(name = "MODEL_ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(fetch = FetchType.LAZY, cascade=CascadeType.ALL, mappedBy = "model")
    private Set<PersistableField> fields;

    public synchronized PersistableModel add(PersistableField field) {
        if(fields == null) {
            fields = newHashSet();
        } else {
            try {
                getField(field.getFieldName());
                throw new IllegalArgumentException(String.format("Unable to add Field to Model. Model already contains a field with name: %s", field.getFieldName()));
            } catch(NoSuchElementException e) {
                // No duplicate field in state, check for key field.
                if(field.isKey()) {
                    try {
                        Field keyField = matchFieldByPredicate(fields, key());
                        throw new IllegalArgumentException(String.format("Unable to add key Field to Model. Model already contains a key field: %s", keyField.getFieldName()));
                    } catch(NoSuchElementException e1) {
                        // There is no prior key field and hence safe to add this as a key field.
                    }
                }
            }
        }
        field.setModel(this);
        fields.add(field);
        return this;
    }

    @Override
    public void add(Field field) {
        add(new PersistableField(field.getFieldName(), field.getFieldValue(), field.isKey()));
    }

    @Override
    public String getFieldValue(String fieldName) {
        Field field = getField(fieldName);
        return field != null ? field.getFieldValue() : null;
    }

    @Override
    public Field getField(final String fieldName) throws NoSuchElementException {
        checkArgument(fieldName != null);
        return matchFieldByPredicate(fields, withName(fieldName));
    }

    /**
     * @throws NoSuchElementException if the match by predicate returned nothing
     * @throws IllegalArgumentException if match by predicate returned more than one field.
     */
    private Field matchFieldByPredicate(Set<PersistableField> fields, Predicate<Field> matchFieldByName) {
        checkState(fields != null);
        return getOnlyElement(filter(fields, matchFieldByName));
    }

    private Predicate<Field> withName(final String fieldName) {
        return new Predicate<Field>() {
            @Override
            public boolean apply(Field input) {
                return fieldName.equals(input.getFieldName());
            }
        };
    }

    @Override
    public Collection<? extends Field> allFields() {
        checkState(fields != null);
        return fields;
    }

    @Override
    public String getKeyFieldValue() {
        return matchFieldByPredicate(fields, key()).getFieldValue();
    }

    private Predicate<Field> key() {
        return new Predicate<Field>() {
            @Override
            public boolean apply(Field input) {
                return input.isKey();
            }
        };
    }

    @Override
    public String toString() {
        return new ReflectionToStringBuilder(this).toString();
    }

    @Override
    public boolean equals(Object that) {
        return EqualsBuilder.reflectionEquals(this, that);
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }
}
